Create a state machine in Home Assistant
I recently found my self in the need of a fairly complex automation. Being a fan of state machines I wanted to try implementing one in Home Assistant.
The problem #
We will look at a simplified version of the above problem to demonstrate how to implement a state machine in Home Assistant. At the end of the post you will find the complete problem.
At the bottom of my staircase I have a door and light that I want to turn on when the door is opened. I want the light to stay on for 2 minutes after the door is closed. To make it more complex we want to abort any timeout and keep the light on if the door is opened again before the light is turned off.
The solution: A state machine #
This diagram illustrates the behavior I want to implement:
Implementation in Home Assistant #
A state machine can be in one out of a given set of states. The input select entity in Home assistant provides us with this exact functionality: It can only be in one state, and you are able to define all the possible states.
We create a new input select entity from Settings > Devices and Services > Helpers and call it Staircase State. We define our set of possible states to be INACTIVE, ACTIVE and FINISHING. We also add a new Timer entity to help us with the timeout functionality.
Creating the automation #
The state machine diagram contains all the information we need. Now we just need to translate it into a Home Assistant automation.
Setup transitions #
We want to transition between states when certain events happens. From the state diagram we see that these event triggers a state transition when
- the door opens (opened)
- the door closes (closed)
- the timer completes (timeout)
We define each of these events as triggers in our automation and give them a trigger id (in parentheses) to be able to distinguish between them in our actions.
Example: Trigger when door opens
as YAML:
type: opened
platform: device
device_id: 1698d3f9359c76ca2bca127994f252c3
entity_id: b70cbe8114e65d919e8fb97b333a8ccd
domain: binary_sensor
id: door_opened
Now that we have our triggers we can add our transition actions. We need four of them:
- If state is INACTIVE and door opens: transition to ACTIVE state
- If state is ACTIVE and door closes: transition to FINISHING state
- If state is FINISHING and door opens: transition to ACTIVE state
- If state is FINISHING and we receive a timeout: transition to INACTIVE
I found it easiest to solve this by using nested if-then
actions. One outer if-then
condition makes sure we are in a given state, while an inner condition is checking which trigger activated the automation. For more complex state machines this makes it easy to have multiple events trigger the same transition. Finally, the action of the inner if-then
block will trigger the select
service of our input select representing the current state.
Example: YAML definition for the first automation action
as YAML:
if:
- condition: state
entity_id: input_select.state_staircase
state: INACTIVE
then:
- if:
- condition: trigger
id:
- opened
then:
- service: input_select.select_option
data:
option: ACTIVE
target:
entity_id: input_select.state_staircase
alias: When INACTIVE
Entry and exit actions #
State machines often have actions they want to perform when entering or existing a state. In our case we have the following:
- Exiting INACTIVE: Turn on the light
- Entering INACTIVE: Turn off the light
- Entering FINISHING: Start timeout timer
- Exiting FINISHING: Cancel timeout timer
We can specify each of these as a new trigger (with a unique id) in our automation.
Example: Create a trigger for entering INACTIVE state
as YAML:
platform: state
entity_id:
- input_select.state_staircase
to: INACTIVE
id: to_inactive
Now that we have triggers for entering and exiting the states that we are interested in, we can add the actions we want in the actions part of the automation. We use an if-then
action to only run an action when entering or exiting a particular state.
For instance: we want to turn the lights off when entering the INACTIVE state, meaning we make sure to only run the action if the automation was triggered by the to_inactive
trigger (defined as an example trigger above). The configuration looks like this:
as YAML:
if:
- condition: trigger
id:
- to_inactive
then:
- type: turn_off
device_id: ...
entity_id: ...
domain: light
alias: Entering INACTIVE (on_entry)
Configure the automation mode #
By default automations runs in single
mode, meaning that only a single instance of the automation will be running at any given time. Any new triggers will be ignored while the automation is running.
This mode will not work for our case because the entry and exist state events will be triggered when the transition actions are running. Changing the automation mode to Queued tells Home Assistant to put all trigger events in a queue and process then in sequence, running the automation once for every trigger, exactly what we want.
Extra: The complete problem #
I'll include the complete problem to show that it can't simply be solved with a simple automation with an delay that restarts every time a person is detected.
At the bottom of my staircase I have a light that I want to turn on when either the door is opened or when motion is detected in the staircase. I want the light to stay on for 2 minutes after motion is no longer detected and the door is closed. To make it more complex we want to abort any timeout and keep the light on if we detect motion or the door is opened again before the light is turned off.
The state diagram looks like this:
And the complete automation looks like this:
alias: State machine for light in stair case
description: ""
trigger:
- type: opened
platform: device
device_id: 1698d3f9359c76ca2bca127994f252c3
entity_id: b70cbe8114e65d919e8fb97b333a8ccd
domain: binary_sensor
id: door_opened
- type: not_opened
platform: device
device_id: 1698d3f9359c76ca2bca127994f252c3
entity_id: b70cbe8114e65d919e8fb97b333a8ccd
domain: binary_sensor
id: door_closed
- type: motion
platform: device
device_id: af1158804ea13cc4209019680ad6725b
entity_id: 2ca0bbfd1760298d721d4311602504b9
domain: binary_sensor
id: motion_detected
- type: no_motion
platform: device
device_id: af1158804ea13cc4209019680ad6725b
entity_id: 2ca0bbfd1760298d721d4311602504b9
domain: binary_sensor
id: motion_stopped
- platform: event
event_type: timer.finished
event_data:
entity_id: timer.timeout_light_staircase
id: timeout
- platform: state
entity_id:
- input_select.state_staircase
to: FINISHING
id: to_finishing
- platform: state
entity_id:
- input_select.state_staircase
id: from_finishing
from: FINISHING
- platform: state
entity_id:
- input_select.state_staircase
to: INACTIVE
id: to_inactive
- platform: state
entity_id:
- input_select.state_staircase
id: from_inactive
from: INACTIVE
condition: []
action:
- if:
- condition: trigger
id:
- to_inactive
then:
- type: turn_off
device_id: 1c8bb6c244b77aee0d8ddb25d1615a03
entity_id: 83b67057766d16b88e879d32355379ab
domain: light
alias: Entering INACTIVE (on_entry)
- if:
- condition: trigger
id:
- from_inactive
then:
- type: turn_on
device_id: 1c8bb6c244b77aee0d8ddb25d1615a03
entity_id: 83b67057766d16b88e879d32355379ab
domain: light
alias: Exiting INACTIVE (on_exit)
- if:
- condition: trigger
id:
- from_finishing
then:
- service: timer.cancel
data: {}
target:
entity_id: timer.timeout_light_staircase
alias: Exiting FINISHING (on_exit)
- if:
- condition: trigger
id:
- to_finishing
then:
- service: timer.start
data:
duration: "00:02:00"
target:
entity_id: timer.timeout_light_staircase
alias: Entering FINISHING (on_entry)
- if:
- condition: state
entity_id: input_select.state_staircase
state: INACTIVE
then:
- if:
- condition: trigger
id:
- door_opened
- motion_detected
then:
- service: input_select.select_option
data:
option: ACTIVE
alias: Transition to ACTIVE
target:
entity_id: input_select.state_staircase
alias: Transition to ACTIVE when motion detected or door opened
alias: When INACTIVE
- if:
- condition: state
entity_id: input_select.state_staircase
state: ACTIVE
then:
- if:
- condition: and
conditions:
- condition: trigger
id:
- door_closed
- motion_stopped
- condition: not
conditions:
- condition: state
entity_id: >-
binary_sensor.smart_motion_sensor_home_security_motion_detection
state: "on"
alias: Make sure there are no motion detected
- condition: not
conditions:
- condition: state
entity_id: binary_sensor.lumi_lumi_sensor_magnet_aq2_on_off
state: "on"
alias: Make sure door is closed
then:
- service: input_select.select_option
data:
option: FINISHING
alias: Transition to FINISHING
target:
entity_id: input_select.state_staircase
alias: Transition to FINISHING when the door is closed and no motion is detected
alias: When ACTIVE
- if:
- condition: state
entity_id: input_select.state_staircase
state: FINISHING
then:
- if:
- condition: trigger
id:
- timeout
then:
- service: input_select.select_option
data:
option: INACTIVE
alias: Transition to INACTIVE
target:
entity_id: input_select.state_staircase
alias: Transition to INACTIVE after timer expires
- if:
- condition: trigger
id:
- door_opened
- motion_detected
then:
- service: input_select.select_option
data:
option: ACTIVE
target:
entity_id: input_select.state_staircase
alias: Transition back to ACTIVE if motion is detected or door is opened
alias: When FINISHING
mode: queued
max: 12